Completed
Push — master ( a31174...af3b49 )
by Antonio
10s
created

Backbone.View.extend.addItem   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 10
dl 0
loc 17
rs 9.9
c 1
b 0
f 1
cc 3
nc 4
nop 1
1
define(
2
    [
3
        'jquery',
4
        'underscore',
5
        'backbone',
6
        'oro/translator',
7
        'routing',
8
        'oro/mediator',
9
        'oro/loading-mask',
10
        'pim/dialog',
11
        'flagbit/JsonGenerator',
12
        'jquery-ui'
13
    ],
14
    function ($, _, Backbone, __, Routing, mediator, LoadingMask, Dialog, JsonGenerator) {
15
        'use strict';
16
17
        var AttributeOptionItem = Backbone.Model.extend({
18
            defaults: {
19
                code: '',
20
                optionValues: {},
21
                constraints: {},
22
                type_config: {}
23
            },
24
            attributesToJson: function() {
25
                if (typeof this.get('constraints') === 'object') {
26
                    this.set('constraints', JSON.stringify(this.get('constraints')));
27
                }
28
                if (typeof this.get('type_config') === 'object') {
29
                    this.set('type_config', JSON.stringify(this.get('type_config')));
30
                }
31
            }
32
        });
33
34
        var ItemCollection = Backbone.Collection.extend({
35
            model: AttributeOptionItem,
36
            initialize: function (options) {
37
                this.url = options.url;
38
            }
39
        });
40
41
        var EditableItemView = Backbone.View.extend({
42
            tagName: 'tr',
43
            className: 'editable-item-row AknGrid-bodyRow',
44
            showTemplate: _.template(
45
                '<td class="AknGrid-bodyCell">' +
46
                    '<span class="handle"><i class="icon-reorder"></i></span>' +
47
                    '<span class="option-code"><%= item.code %></span>' +
48
                '</td>' +
49
                '<% _.each(locales, function (locale) { %>' +
50
                '<td class="AknGrid-bodyCell">' +
51
                    '<% if (item.optionValues[locale]) { %>' +
52
                        '<span title="<%= item.optionValues[locale].value %>">' +
53
                            '<%= item.optionValues[locale].value %>' +
54
                        '</span>' +
55
                    '<% } %>' +
56
                '</td>' +
57
                '<% }); %>' +
58
                '<td class="AknGrid-bodyCell">' +
59
                    '<% _.each(types, function (type) { %>' +
60
                        '<% if (item.type == type.type) { %>' +
61
                            '<span class="option-type">' +
62
                                '<%= type.label %>' +
63
                            '</span>' +
64
                        '<% } %>' +
65
                    '<% }); %>' +
66
                '</td>' +
67
                '<td class="AknGrid-bodyCell">' +
68
                    '<span class="option-constraint json-generator json-constraint-generator"><%= item.constraints %></span>' +
69
                '</td>' +
70
                '<td class="AknGrid-bodyCell">' +
71
                    '<span class="option-config json-generator json-<%= item.type %>-generator"><%= item.type_config %></span>' +
72
                '</td>' +
73
                '<td class="AknGrid-bodyCell">' +
74
                    '<div class="AknButtonList AknButtonList--right">' +
75
                        '<span class="btn btn-small AknButtonList-item AknIconButton AknIconButton--small AknIconButton--apply edit-row"><i class="icon-pencil"></i></span>' +
76
                        '<span class="btn btn-small AknButtonList-item AknIconButton AknIconButton--small AknIconButton--important delete-row"><i class="icon-trash"></i></span>' +
77
                    '</div>' +
78
                '</td>'
79
            ),
80
            editTemplate: _.template(
81
                '<td class="AknGrid-bodyCell field-cell">' +
82
                    '<div class="AknFieldContainer-inputContainer">' +
83
                        '<input type="text" class="attribute_option_code exclude AknTextField" value="<%= item.code %>"/>' +
84
                        '<i class="validation-tooltip hidden AknIconButton AknIconButton--hide AknIconButton--important" data-placement="top" data-toggle="tooltip"></i>' +
85
                    '</div>' +
86
                '</td>' +
87
                '<% _.each(locales, function (locale) { %>' +
88
                '<td class="AknGrid-bodyCell field-cell">' +
89
                    '<% if (item.optionValues[locale]) { %>' +
90
                        '<input type="text" class="attribute-option-value exclude AknTextField" data-locale="<%= locale %>" ' +
91
                            'value="<%= item.optionValues[locale].value %>"/>' +
92
                    '<% } else { %>' +
93
                        '<input type="text" class="attribute-option-value exclude AknTextField" data-locale="<%= locale %>" ' +
94
                            'value=""/>' +
95
                    '<% } %>' +
96
                '</td>' +
97
                '<% }); %>' +
98
                '<td class="AknGrid-bodyCell">' +
99
                    '<select class="attribute_option_type exclude" >' +
100
                        '<% _.each(types, function (type) { %>' +
101
                            '<option value="<%= type.type %>" <% if (item.type == type.type) { %> selected="selected" <% } %>><%= type.label %></option>' +
102
                        '<% }); %>' +
103
                    '</select>' +
104
                '</td>' +
105
                '<td class="AknGrid-bodyCell">' +
106
                    '<textarea class="attribute_option_constraints exclude json-generator json-constraint-generator" ><%= item.constraints %></textarea>' +
107
                '</td>' +
108
                '<td class="AknGrid-bodyCell">' +
109
                    '<textarea class="attribute_option_config exclude json-generator json-<%= item.type %>-generator" ><%= item.type_config %></textarea>' +
110
                '</td>' +
111
                '<td class="AknGrid-bodyCell">' +
112
                    '<div class="AknButtonList AknButtonList--right">' +
113
                        '<span class="btn btn-small AknButtonList-item AknIconButton AknIconButton--small AknIconButton--apply update-row"><i class="icon-ok"></i></span>' +
114
                        '<span class="btn btn-small AknButtonList-item AknIconButton AknIconButton--small AknIconButton--important show-row"><i class="icon-remove"></i></span>' +
115
                    '</div>' +
116
                '</td>'
117
            ),
118
            events: {
119
                'click .show-row':   'stopEditItem',
120
                'click .edit-row':   'startEditItem',
121
                'click .delete-row': 'deleteItem',
122
                'click .update-row': 'updateItem',
123
                'keyup input':       'soil',
124
                'keydown':           'cancelSubmit',
125
                'change .attribute_option_type': 'changeType'
126
            },
127
            editable: false,
128
            parent: null,
129
            loading: false,
130
            locales: [],
131
            initialize: function (options) {
132
                this.locales       = options.locales;
133
                this.parent        = options.parent;
134
                this.model.urlRoot = this.parent.updateUrl;
135
                this.types = [{
136
                    'type': 'text',
137
                    'label': __('pim_enrich.entity.attribute.type.pim_catalog_text')
138
                }, {
139
                    'type': 'number',
140
                    'label': __('pim_enrich.entity.attribute.type.pim_catalog_number')
141
                }, {
142
                    'type':  'select',
143
                    'label': __('pim_enrich.entity.attribute.type.pim_catalog_simpleselect')
144
                }, {
145
                    'type':  'select_from_url',
146
                    'label': __('pim_enrich.entity.attribute.type.pim_catalog_simpleselect_from_url')
147
                }];
148
149
                this.render();
150
            },
151
            render: function () {
152
                var template = null;
0 ignored issues
show
Unused Code introduced by
The assignment to template seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
153
                var types = this.types;
154
155
                if (this.editable) {
156
                    this.clean();
157
                    this.$el.addClass('in-edition');
158
                    template = this.editTemplate;
159
                } else {
160
                    this.$el.removeClass('in-edition');
161
                    template = this.showTemplate;
162
                }
163
164
                this.model.attributesToJson();
165
                this.$el.html(template({
166
                    item: this.model.toJSON(),
167
                    locales: this.locales,
168
                    types: types
169
                }));
170
171
                this.$el.find('.json-generator').each(function() {
172
                    new JsonGenerator(this, types);
0 ignored issues
show
Unused Code Best Practice introduced by
The object created with new JsonGenerator(this, types) is not used but discarded. Consider invoking another function instead of a constructor if you are doing this purely for side effects.
Loading history...
173
                }).bind(types);
174
175
                this.$el.attr('data-item-id', this.model.id);
176
177
                return this;
178
            },
179
            changeType: function () {
180
                this.inLoading(true);
181
                var editedModel = this.loadModelFromView();
182
                this.model.set(editedModel.attributes);
183
                this.render();
184
                this.inLoading(false);
185
                this.clean();
186
            },
187
            showReadableItem: function () {
188
                this.editable = false;
189
                this.parent.showReadableItem(this);
190
                this.clean();
191
                this.render();
192
            },
193
            showEditableItem: function () {
194
                this.editable = true;
195
                this.render();
196
                this.model.set(this.loadModelFromView().attributes);
197
            },
198
            startEditItem: function () {
199
                var rowIsEditable = this.parent.requestRowEdition(this);
200
201
                if (rowIsEditable) {
202
                    this.showEditableItem();
203
                }
204
            },
205
            stopEditItem: function () {
206
                if (!this.model.id || this.dirty) {
207
                    if (this.dirty) {
208
                        Dialog.confirm(
209
                            __('confirm.attribute_option.cancel_edition_on_new_option_text'),
210
                            __('confirm.attribute_option.cancel_edition_on_new_option_title'),
211
                            function () {
212
                                this.showReadableItem(this);
213
                                if (!this.model.id) {
214
                                    this.deleteItem();
215
                                }
216
                            }.bind(this));
217
                    } else {
218
                        if (!this.model.id) {
219
                            this.deleteItem();
220
                        } else {
221
                            this.showReadableItem();
222
                        }
223
                    }
224
                } else {
225
                    this.showReadableItem();
226
                }
227
            },
228
            deleteItem: function () {
229
                var itemCode = this.el.firstChild.innerText;
230
231
                Dialog.confirm(
232
                    __('pim_enrich.item.delete.confirm.content', {'itemName': itemCode}),
233
                    __('pim_enrich.item.delete.confirm.title', {'itemName': itemCode}),
234
                    function () {
235
                        this.parent.deleteItem(this);
236
                    }.bind(this)
237
                );
238
            },
239
            updateItem: function () {
240
                this.inLoading(true);
241
242
                var editedModel = this.loadModelFromView();
243
244
                editedModel.save(
245
                    {},
246
                    {
247
                        url: this.model.url(),
248
                        success: function () {
249
                            this.inLoading(false);
250
                            this.model.set(editedModel.attributes);
251
                            this.clean();
252
                            this.stopEditItem();
253
                        }.bind(this),
254
                        error: function (data, xhr) {
255
                            this.inLoading(false);
256
257
                            var response = xhr.responseJSON;
258
                            var _response = response;
259
                            if(_response.children) {
260
                                _response = _response.children;
261
                            }
262
                            if (_response && _response.code) {
263
                                var error = _response.code;
0 ignored issues
show
Unused Code introduced by
The variable error seems to be never used. Consider removing it.
Loading history...
264
                                var message = '';
265
                                if(_response.code) {
266
                                    if(_response.code.errors) {
267
                                        message = _response.code.errors.join('<br/>');
268
                                    }
269
                                    else {
270
                                        message = _response.code;
271
                                    }
272
                                }
273
                                this.$el.find('.validation-tooltip')
274
                                    .addClass('visible')
275
                                    .tooltip('destroy')
276
                                    .tooltip({title: message})
277
                                    .tooltip('show');
278
279
                                this.$el.find('.AknIconButton--hide')
280
                                    .removeClass('AknIconButton--hide')
281
                                    .addClass('icon-warning-sign');
282
283
                            } else {
284
                                Dialog.alert(
285
                                    __('alert.attribute_option.error_occured_during_submission'),
286
                                    __('error.saving.attribute_option')
287
                                );
288
                            }
289
                        }.bind(this)
290
                    }
291
                );
292
            },
293
            cancelSubmit: function (e) {
294
                if (e.keyCode === 13) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if e.keyCode === 13 is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
295
                    this.updateItem();
296
297
                    return false;
298
                }
299
            },
300
            loadModelFromView: function () {
301
                var attributeOptions = {};
302
                var editedModel = this.model.clone();
303
304
                editedModel.urlRoot = this.model.urlRoot;
305
306
                _.each(this.$el.find('.attribute-option-value'), function (input) {
307
                    var locale = input.dataset.locale;
308
309
                    attributeOptions[locale] = {
310
                        locale: locale,
311
                        value:  input.value,
312
                        id:     this.model.get('optionValues')[locale] ?
313
                            this.model.get('optionValues')[locale].id :
314
                            null
315
                    };
316
                }.bind(this));
317
318
                editedModel.set('code', this.$el.find('.attribute_option_code').val());
319
                editedModel.set('optionValues', attributeOptions);
320
                editedModel.set('type', this.$el.find('.attribute_option_type').val());
321
                try {
322
                    editedModel.set('constraints', JSON.parse(this.$el.find('.attribute_option_constraints').val()));
323
                    editedModel.set('type_config', JSON.parse(this.$el.find('.attribute_option_config').val()));
324
                } catch (e) {
325
                    Dialog.alert(
326
                        __('flagbit.attribute_table.alert.json_error_text'),
327
                        __('flagbit.attribute_table.alert.json_error_title')
328
                    );
329
                }
330
331
                return editedModel;
332
            },
333
            inLoading: function (loading) {
334
                this.parent.inLoading(loading);
335
            },
336
            soil: function () {
337
                if (JSON.stringify(this.model.attributes) !== JSON.stringify(this.loadModelFromView().attributes)) {
338
                    this.dirty = true;
339
                } else {
340
                    this.dirty = false;
341
                }
342
            },
343
            clean: function () {
344
                this.dirty = false;
345
            }
346
        });
347
348
        var ItemCollectionView = Backbone.View.extend({
349
            tagName: 'table',
350
            className: 'table table-bordered table-stripped attribute-option-view AknGrid AknGrid--unclickable',
351
            template: _.template(
352
                '<!-- Pim/Bundle/EnrichBundle/Resources/public/js/pim-attributeoptionview.js -->' +
353
                '<colgroup>' +
354
                    '<col class="code" span="1"></col>' +
355
                    '<col class="fields" span="<%= locales.length %>"></col>' +
356
                    '<col class="type" span="1"></col>' +
357
                    '<col class="constaint" span="1"></col>' +
358
                    '<col class="config" span="1"></col>' +
359
                    '<col class="action" span="1"></col>' +
360
                '</colgroup>' +
361
                '<thead>' +
362
                    '<tr>' +
363
                        '<th class="AknGrid-headerCell"><%= code_label %></th>' +
364
                        '<% _.each(locales, function (locale) { %>' +
365
                        '<th class="AknGrid-headerCell">' +
366
                            '<%= locale %>' +
367
                        '</th>' +
368
                        '<% }); %>' +
369
                        '<th class="AknGrid-headerCell"><%= type_label %></th>' +
370
                        '<th class="AknGrid-headerCell"><%= constraint_label %></th>' +
371
                        '<th class="AknGrid-headerCell"><%= config_label %></th>' +
372
                        '<th class="AknGrid-headerCell">Action</th>' +
373
                    '</tr>' +
374
                '</thead>' +
375
                '<tbody></tbody>' +
376
                '<tfoot>' +
377
                    '<tr>' +
378
                        '<td colspan="<%= 5 + locales.length %>">' +
379
                            '<span class="btn AknButton AknButton--apply AknButton--small option-add pull-right"><%= add_column_label %></span>' +
380
                        '</td>' +
381
                    '</tr>' +
382
                '</tfoot>'
383
            ),
384
            events: {
385
                'click .option-add': 'addItem'
386
            },
387
            $target: null,
388
            locales: [],
389
            sortable: true,
390
            sortingUrl: '',
391
            updateUrl: '',
392
            currentlyEditedItemView: null,
393
            itemViews: [],
394
            rendered: false,
395
            initialize: function (options) {
396
                this.$target    = options.$target;
397
                this.collection = new ItemCollection({url: options.updateUrl});
398
                this.locales    = options.locales;
399
                this.updateUrl  = options.updateUrl;
400
                this.sortingUrl = options.sortingUrl;
401
                this.sortable   = options.sortable;
402
403
                this.render();
404
                this.load();
405
            },
406
            render: function () {
407
                this.$el.empty();
408
409
                this.currentlyEditedItemView = null;
410
                this.updateEditionStatus();
411
412
                this.$el.html(this.template({
413
                    locales: this.locales,
414
                    add_column_label: __('label.attribute_table.add_column'),
415
                    code_label: __('Code'),
416
                    type_label: __('flagbit_attribute_table_type_label'),
417
                    constraint_label: __('flagbit_attribute_table_validation_label'),
418
                    config_label: __('flagbit_attribute_table_config_label')
419
                }));
420
421
                _.each(this.collection.models, function (attributeOptionItem) {
422
                    this.addItem({item: attributeOptionItem});
423
                }.bind(this));
424
425
                if (0 === this.collection.length) {
426
                    this.addItem();
427
                }
428
429
                if (!this.rendered) {
430
                    this.$target.html(this.$el);
431
432
                    this.rendered = true;
433
                }
434
435
                this.tbodyElement = this.$el.find('tbody');
436
437
                this.tbodyElement.sortable({
438
                    axis: 'y',
439
                    distance: 5,
440
                    cursor: 'move',
441
                    scroll: true,
442
                    start: function(e, ui){
443
                        ui.placeholder.height($(ui.item[0]).height());
444
                    },
445
                    helper: function (e, ui) {
446
                        ui.children().each(function () {
447
                            $(this).width($(this).width());
448
                        });
449
450
                        return ui;
451
                    },
452
                    stop: function () {
453
                        this.updateSorting();
454
                    }.bind(this)
455
                });
456
457
                this.updateSortableStatus(this.sortable);
458
459
                return this;
460
            },
461
            load: function () {
462
                this.itemViews = [];
463
                this.inLoading(true);
464
                this.collection
465
                    .fetch({
466
                        success: function () {
467
                            this.inLoading(false);
468
                            this.render();
469
                        }.bind(this)
470
                    });
471
            },
472
            addItem: function (opts) {
473
                var options = opts || {};
474
475
                //If no item model provided we create one
476
                var itemToAdd;
477
                if (!options.item) {
478
                    itemToAdd = new AttributeOptionItem();
479
                } else {
480
                    itemToAdd = options.item;
481
                }
482
483
                var newItemView = this.createItemView(itemToAdd);
484
485
                if (newItemView) {
486
                    this.$el.children('tbody').append(newItemView.$el);
487
                }
488
            },
489
            createItemView: function (item) {
490
                var itemView = new EditableItemView({
491
                    model:    item,
492
                    url:      this.updateUrl,
493
                    locales:  this.locales,
494
                    parent:   this
495
                });
496
497
                //If the item is new the view is changed to edit mode
498
                if (!item.id) {
499
                    if (!this.requestRowEdition(itemView)) {
500
                        return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
501
                    } else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
502
                        itemView.showEditableItem();
503
                    }
504
                }
505
506
                this.collection.add(item);
507
                this.itemViews.push(itemView);
508
509
                return itemView;
510
            },
511
            requestRowEdition: function (attributeOptionRow) {
512
                if (this.currentlyEditedItemView) {
513
                    if (this.currentlyEditedItemView.dirty) {
514
                        Dialog.alert(__('alert.attribute_option.save_before_edit_other'));
515
516
                        return false;
517
                    } else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
518
                        this.currentlyEditedItemView.stopEditItem();
519
                        this.currentlyEditedItemView = null;
520
                        this.updateEditionStatus();
521
                    }
522
                }
523
524
                if (attributeOptionRow.model.id) {
525
                    this.currentlyEditedItemView = attributeOptionRow;
526
                }
527
528
                this.updateEditionStatus();
529
530
                return true;
531
            },
532
            showReadableItem: function (item) {
533
                if (item === this.currentlyEditedItemView) {
534
                    this.currentlyEditedItemView = null;
535
                    this.updateEditionStatus();
536
                }
537
            },
538
            deleteItem: function (item) {
539
                this.inLoading(true);
540
541
                item.model.destroy({
542
                    success: function () {
543
                        this.inLoading(false);
544
545
                        this.collection.remove(item);
546
                        this.currentlyEditedItemView = null;
547
                        this.updateEditionStatus();
548
549
                        if (0 === this.collection.length) {
550
                            this.addItem();
551
                            item.$el.hide(0);
552
                        } else if (!item.model.id) {
553
                            item.$el.hide(0);
554
                        } else {
555
                            item.$el.hide(500);
556
                        }
557
                    }.bind(this),
558
                    error: function (data, response) {
559
                        this.inLoading(false);
560
                        var message;
561
562
                        if (response.responseJSON) {
563
                            message = response.responseJSON.message;
564
                        } else {
565
                            message = response.responseText;
566
                        }
567
568
                        Dialog.alert(message, __('error.removing.attribute_option'));
569
                    }.bind(this)
570
                });
571
            },
572
            updateEditionStatus: function () {
573
                if (this.currentlyEditedItemView) {
574
                    this.$el.addClass('in-edition');
575
                } else {
576
                    this.$el.removeClass('in-edition');
577
                }
578
            },
579
            updateSortableStatus: function (sortable) {
580
                this.sortable = sortable;
581
582
                if (sortable) {
583
                    this.tbodyElement.sortable('enable');
584
                } else {
585
                    this.tbodyElement.sortable('disable');
586
                }
587
            },
588
            updateSorting: function () {
589
                this.inLoading(true);
590
                var sorting = [];
591
592
                var rows = this.$el.find('tbody tr.editable-item-row');
593
                for (var i = rows.length - 1; i >= 0; i--) {
594
                    sorting[i] = rows[i].dataset.itemId;
595
                }
596
597
                $.ajax({
598
                    url: this.sortingUrl,
599
                    type: 'PUT',
600
                    data: JSON.stringify(sorting)
601
                }).done(function () {
602
                    this.inLoading(false);
603
                }.bind(this));
604
            },
605
            inLoading: function (loading) {
606
                if (loading) {
607
                    var loadingMask = new LoadingMask();
608
                    loadingMask.render().$el.appendTo(this.$el);
609
                    loadingMask.show();
610
                } else {
611
                    this.$el.find('.loading-mask').remove();
612
                }
613
            }
614
        });
615
616
        return function ($element) {
617
            var itemCollectionView = new ItemCollectionView(
618
            {
619
                $target: $element,
620
                updateUrl: Routing.generate(
621
                    'pim_enrich_attributeoption_index',
622
                    {attributeId: $element.data('attribute-id')}
623
                ),
624
                sortingUrl: Routing.generate(
625
                    'pim_enrich_attributeoption_update_sorting',
626
                    {attributeId: $element.data('attribute-id')}
627
                ),
628
                locales: $element.data('locales'),
629
                sortable: $element.data('sortable')
630
            });
631
632
            mediator.on('attribute:auto_option_sorting:changed', function (autoSorting) {
633
                itemCollectionView.updateSortableStatus(!autoSorting);
634
            }.bind(this));
0 ignored issues
show
unused-code introduced by
The call to bind does not seem necessary since the function does not use this. Consider calling it directly.
Loading history...
635
        };
636
    }
637
);
638